-
Notifications
You must be signed in to change notification settings - Fork 27
RSDK-9298: Instance class #374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/viam/sdk/module/service.cpp
Outdated
| std::shared_ptr<Resource> ModuleService::get_parent_resource_(const Name& name) { | ||
| if (!parent_) { | ||
| parent_ = RobotClient::at_local_socket(parent_addr_, {0, boost::none}); | ||
| parent_ = RobotClient::at_local_socket(parent_addr_, {0, boost::none}, registry_); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question for reviewers--I am wondering if RobotClient should be part of the Instance as well, it seems like we always want it? Looking through the examples I believe the only scenario where RobotClient is not constructed explicitly is one where we just construct a ModuleService but that ends up constructing a RobotClient in this method anyway.
Looking ahead to the logger PR, the logger will need a RobotClient for the gRPC Log call, so we may want to avoid a situation where RobotClient is getting delayed instantiation and just construct one on application startup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd prefer to keep RobotClient distinct.
- I think of the
Instanceas being a bit of a black box; the user knows they need one and that they need to keep it around, but they don't need to know anything about what's inside. By contrast, aRobotClientis something that users may interact with directly and frequently. - There should only ever be one live instance, whereas one could imagine a user writing client code that interacts with multiple robots and so therefore has multiple robot clients. We could have some sort of map from name to RobotClient inside the
instancebut then a user starts needing to know aboutInstanceAPIs and we're adding new layers of abstraction between the user and their access to robot APIs and I think it just starts to get a bit complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those points make sense. Follow-ups
- if there are multiple
RobotClientshould log messages be getting sent to theLogcall of each of them? - given the comment about
Instanceinsulation, how do we feel about now having to doinst.registry()for some of the constructors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm good questions!
if there are multiple RobotClient should log messages be getting sent to the Log call of each of them?
I think this is a question we can largely ignore, thankfully. If there are multiple RobotClients then the user is interacting with purely client code and so the logs should be showing up in the shell, so we dont have to call a RobotClient::log at all.
If on the other hand, we do have to send logs via the Log method then we're dealing with a module. But, each module is started up as its own process for a single Robot, so we can always only send logs via the Log call to the module's parent.
given the comment about Instance insulation, how do we feel about now having to do inst.registry() for some of the constructors?
Caveat: I will readily confess to not being particularly hip to C++ conventions. If what I'm saying flies in the face of idiomatic C++ design then obviously we should prefer remaining idiomatic.
I personally prefer to not do that. Per our offline conversation yesterday and to reiterate some of that: I'd love to hide as much as possible from users about the instance, and ideally not really expose any methods at all to the user about it. If it's needed in particular cases, I'd prefer to just pass the instance as an argument and let the RobotClient constructor, e.g., deal with getting the Registry from a private method via some friend class setup.
I don't think this is always clean and easy to do, as when registering custom resources, but I imagine we could add a register_resource method to a RobotClient that under the hood interacts with the registry. I think this is a bit cleaner and a bit more intuitive (when you want a robot client to deal with a custom resource, you register that resource with the client!). A possible downside here is this pattern is slightly misleading: we're saying "register it with the robot client" but in fact we're registering with the singleton registry, which means another robot client would also be able to work with that resource type. But I don't think this is particularly harmful, and we can work around it if it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah that's helpful--in that case I can have a log backend which binds to a RobotClient but only register it with the logging core when that RobotClient is the parent of a ModuleService !
As for the other stuff I've made some further updates which I think clean up the usage a lot, using the current method as discussed yesterday
stuqdog
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, this is super exciting! Thanks so much for putting this together, it's a complicated one but a big win.
I've done a first pass; there's a lot to think about here so I think there will be more back and forth but here are some initial thoughts.
src/viam/sdk/robot/client.hpp
Outdated
| status get_machine_status() const; | ||
|
|
||
| private: | ||
| Registry* registry_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(minor) I don't feel too strongly, but is this change necessary? It seems to me that we could just use Registry::get() in the refresh() call rather than holding onto this pointer. It might be marginally less performant to have to get the Registry every time we refresh, but I don't expect it's a significant cost and it helps simplify the info that a user is exposed to in the headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe cc @acmorrow when he gets a sec, but wondering if Registry can just get friend access to instance and be able to return a reference to the registry member rather than having to access it through Instance::current with the atomic::loads. My though is that since we say it's UB to use any Viam types without an Instance already constructed we can skip the whole "create if not exists" logic of Instance::current
I agree that the performance penalty can probably be ignored here, but if so then we could do
static Registry& Registry::get()
{
static Registry& result = Instance::current().impl_->registry;
return result;
}
in which case the static would only be initialized one-and-done style
|
|
||
| // Construct the module service and tell it where to place the socket path. | ||
| auto module_service = std::make_shared<vsdk::ModuleService>(socket_path); | ||
| auto module_service = std::make_shared<vsdk::ModuleService>(socket_path, inst.registry()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this change is out of date? It doesn't look like the ModuleService takes a registry as part of its constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops yes good catch, tflite is not built in CI and hadn't rebuilt it locally
src/viam/sdk/common/instance.hpp
Outdated
| /// @brief Instance management for Viam C++ SDK applications. | ||
| /// This is a single instance class which is responsible for global setup and teardown related to | ||
| /// the SDK. An Instance must be constructed before doing anything else in a program, and it must | ||
| /// remain alive in a valid state for the duration of the program. Creating multiple overlapping |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How difficult would it be to get rid of the overlapping clause and just make it an error to create multiple instances, period?
When we create an instance we create a new registry and run a second registry::initialize(), which sets up gRPC reflection a second time. I could easily imagine this getting pretty complicated and having unforeseen side effects. It's definitely an edge case, but I think just being upfront that users can only ever create one instance helps simplify things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no difficulty per se, we can just tell people "don't ever do this, it's UB" 🙂
src/viam/sdk/module/service.cpp
Outdated
|
|
||
| const std::shared_ptr<const ModelRegistration> reg = Registry::lookup_model(cfg.name()); | ||
| const std::shared_ptr<const ModelRegistration> reg = | ||
| parent.registry_->lookup_model(cfg.name()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above re: parent.registry_
src/viam/sdk/module/service.cpp
Outdated
|
|
||
| const std::shared_ptr<const ModelRegistration> reg = | ||
| Registry::lookup_model(cfg.api(), cfg.model()); | ||
| parent.registry_->lookup_model(cfg.api(), cfg.model()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above re: parent.registry_
src/viam/sdk/module/service.hpp
Outdated
| std::string const& resource_name); | ||
| std::shared_ptr<Resource> get_parent_resource_(const Name& name); | ||
|
|
||
| Registry* registry_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above comments on this being a class data member.
src/viam/sdk/registry/registry.hpp
Outdated
| static std::unordered_map<API, std::shared_ptr<const ResourceClientRegistration>> client_apis_; | ||
| static std::unordered_map<API, std::shared_ptr<const ResourceServerRegistration>> server_apis_; | ||
| mutable std::mutex lock_; | ||
| bool initialized_{false}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(minor) I think we can get rid of this entirely now. Now that we have a single instance of a registry that gets automatically initialized when the Instance object is created, we don't need to track this anymore.
| if (current == reinterpret_cast<Instance*>(&sentinel)) { | ||
| throw Exception("instance was destroyed"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(q) pardon my ignorance, I don't think I fully understand this. Under what conditions will this exception throw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait actually this gets to your comment above about making it an error to have multiple instances even if non-overlapping, so it already is an error to have multiple of them
Co-authored-by: Ethan <[email protected]>
Co-authored-by: Ethan <[email protected]>
src/viam/sdk/common/instance.hpp
Outdated
| Instance(); | ||
| ~Instance(); | ||
|
|
||
| static Instance& current(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to doc-comment this, particularly if it is mostly intended for testing.
src/viam/sdk/registry/registry.cpp
Outdated
| }; | ||
|
|
||
| Registry& Registry::get() { | ||
| static Registry& result = Instance::current().impl_->registry; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This will bring
Instanceinto existence even if one wasn't created, I think. Those maybe aren't the right semantics, since then a user of the library could get away with not creating anInstancethemselves. - Why the static& rather than just calling
Instance::current()each time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I think you're right that those aren't the semantics we want--maybe it should be something like
Instance::current(bool create_if_not_exists = false) - This was the result of a discussion me and @stuqdog were going back and forth on, and we thought that
Instanceshould be a total black box so that it's in some sense an implementation detail thatInstancehas-aRegistryor, later,Logging-Core. In either case though, I think that the divergences from our use cases and Mongo's mean thatInstance::currentdoes not end up being unit tests only. But from the "black box" perspective, the user doesn't know thatInstancehas aRegistry, they just know 1) I have to create anInstancebefore doing anything, and 2) I can only access an instance ofRegistrythrough its staticgetso I can't create them as I please
| grpc::reflection::InitProtoReflectionServerBuilderPlugin(); | ||
| } | ||
|
|
||
| std::unordered_map<std::string, std::shared_ptr<const ModelRegistration>> Registry::resources_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| class Registry { | ||
| public: | ||
| /// @brief Get the application-wide instance of Registry. | ||
| static Registry& get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or instance? I also wonder a bit why to expose this on Registry rather than on Instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair question--elaborated in another comment
stuqdog
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, a couple nits but otherwise lgtm!
src/viam/sdk/common/instance.hpp
Outdated
| ~Instance(); | ||
|
|
||
| /// @brief Get the current Instance according to the Creation behavior. | ||
| /// Calling current(Creation::open_existing) when an instance has not yet been constructed is an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(minor) I'd put the error case in a @throws line for clearer doxygen docs.
src/viam/sdk/common/instance.cpp
Outdated
| #include <viam/sdk/common/private/instance.hpp> | ||
| #include <viam/sdk/registry/registry.hpp> | ||
|
|
||
| #include <atomic> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) can we put this include block above the viam/sdk include block?
Introduces a one-and-only
Instanceclass for the SDK. For the time being this only has aRegistry, but it will pave the way for inclusion of better logging and resource-level logging in particular.Some of the machinery in
instance.cppfor singleton management was adapted from https://github.com/mongodb/mongo-cxx-driver/blob/02ad73bc306eb9044eb691de96ad41c13107738d/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/instance.cppA few classes were updated to now get a pointer to a
Registryrather than calling the no-longer-staticRegistrymethods. The existence ofInstance::current()means technically these could just get theRegistryfrom the current instance, or possibly thatRegistrycould have astatic Registry& current()method...open to feedback here.Every example has been updated to define an instance object, with the same boilerplate comment put in front explaining its usage